Repository navigation
feat(reindex): metadata-only mode to backfill MongoDB missing_params (#2008) - #2213
Conversation
…2008) A `metadataOnly` $reindex recomputes each resource's `missing_params` with the rebuild's extraction and version guard and writes nothing to search_index or search_index_contained. `skipExistingMetadata` leaves resources that already carry the field alone. - ReindexTarget gains supports_search_metadata/write_search_metadata_page; only MongoDB (search not offloaded) implements them, everything else is 501 before a job starts. - $reindex-status reports metadataOnly and metadataUpdated. - Rejected with clearExisting, clearOnly or a bulk index rebuild; combines with type scope and idStart/idEnd. - Upgrade step 3 in docs/mongodb/search-indexes.md now uses it.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
…#2008) - Driver unit tests: metadata goes only through writers that keep it, failures are recorded, metadataUpdated/metadataOnly in status, the default trait methods refuse, and the validation branches. - MongoDB: a 40-resource page on a multi-thread runtime takes the prepare-pool extraction path; PageFailure::fan_out_metadata replaces the inline closure and has its own unit test.
#2008) Since #1999 a `missing_params` array is trusted only with the current `missing_params_fp`: arrays written by #1984 (v0.2.5) carry no stamp, and every `:missing=true` on their type takes the value-index route. The skip guard was `missing_params: {$exists: false}`, so on an upgraded tenant it skipped every resource (`metadataUpdated=0`) and left the slow route in place. At 100k Observations, `subject:missing=true` stayed at ~970 ms. The guard now skips a resource only when its array exists and carries the fingerprint this extraction stamps. A failed extraction keeps the old guard. Docs: the #1999 upgrade note points at the metadata-only reindex.
Review: tested with and without, plus perfAt the PR head (old base 3281155)The claims hold: 82 MongoDB After merging main: two problems, both fixed and pushedThe branch had gone stale against #2181 (the
After the fixes: 111 MongoDB Performance: 100k Observations (1.8M
|
| Run | Time | metadataUpdated |
Index rows |
|---|---|---|---|
Full $reindex (main's only remedy) |
106–146 s | – | rewritten, 1.8M |
metadataOnly on an upgraded (unstamped) corpus |
24.8 s | 100,000 | untouched |
metadataOnly + skip on a pre-#1984 corpus |
29.0 s | 100,000 | untouched |
metadataOnly + skip, all current (no-op) |
20.0 s | 0 | untouched |
metadataOnly + skip on unstamped, before the fix |
24.1 s | 0 | untouched |
:missing=true p50 latency, selective case (subject:missing=true, 0 hits):
| State | p50 |
|---|---|
| Upgraded corpus (unstamped arrays) | 969 ms |
After metadataOnly + skip, before the fix |
985 ms (unchanged) |
After metadataOnly |
6.6 ms |
| Pre-#1984 → after backfill | 1,222 ms → 1.5 ms |
- Backfill: about 4–5× faster than a full rebuild, and the index is untouched.
- No-op runs: a skip run with nothing to write still costs about 80% of a real run, because every resource is read and extracted. With the fingerprint, the follow-up you listed (pushing the skip filter into the walk) could now use
missing_params_fp != current. - Normal rebuilds: no measurable change. The timing spread between runs came from host load, and seeding, which this PR doesn't touch, varied by the same ~25%. The only code change on that path is one
Optioncheck per page.
Minor, not changed
Older nodes silently ignore unknown $reindex parameters. During a rolling upgrade, a metadataOnly request that reaches an old node becomes a full rebuild. A sentence in the upgrade steps (send it to an upgraded node) would cover it.
Re-test after merging main (524ac02)I merged main (e99b233: #2218, #2231, #2232) into the branch. There were no conflicts, and none of the new commits touch the reindex code. Then I re-ran the tests and the bench with and without this PR, on a MongoDB 7 replica set. Tests
The Performance: 100k Observations (1.8M
|
| Run | this PR | main |
|---|---|---|
metadataOnly + skipExistingMetadata on an upgraded (unstamped) corpus |
21.1 s, metadataUpdated=100000 |
n/a |
metadataOnly on an upgraded corpus |
17.8 s | n/a |
metadataOnly + skip, everything already current (no-op) |
17.5 s, metadataUpdated=0 |
n/a |
metadataOnly + skip on a pre-#1984 corpus |
25.2 s | n/a |
Full $reindex (main's only remedy) |
89–99 s | 105–130 s |
subject:missing=true p50 (0 hits):
| State | Before | After metadataOnly |
|---|---|---|
| Upgraded corpus (unstamped arrays) | 637–918 ms | 0.9–1.6 ms |
| Pre-#1984 corpus (no metadata) | 910 ms | 1.0 ms |
- Backfill:
metadataOnlyis about 5× faster than a full rebuild, and the search index is left untouched (row count unchanged). - Full rebuilds: no regression. This PR's numbers were slightly below main's, which is host noise; seeding alone varied between 92 s and 128 s.
- No-op skip runs: still cost about 80% of a real run, because every resource is read and extracted (see my earlier comment).
The rolling-upgrade note from my earlier comment still applies: an older node ignores metadataOnly and runs a full rebuild.
Closes #2008.
What
A metadata-only
$reindexmode that fills inmissing_paramson MongoDB resources written before #1984, without rebuilding the value index.{"resourceType": "Parameters", "parameter": [ {"name": "metadataOnly", "valueBoolean": true}, {"name": "skipExistingMetadata", "valueBoolean": true} ]}metadataOnly: same walk, same extraction (search_index_documents_checked) and same version guard (resource_version_filter,upsert: false) as a rebuild. Only theresources.missing_paramsupdate runs; nothing is deleted from or inserted intosearch_index/search_index_contained.skipExistingMetadata: addsmissing_params: {$exists: false}to each update's filter, so resources that already carry the field are left alone.$reindex-statusaddsmetadataOnly: trueandmetadataUpdated, the number of resources the updates matched.entriesCreatedstays 0. Per-resource failures are reported as they are today.clearExisting,clearOnlyor a bulk index rebuild, and 400 forskipExistingMetadatawithoutmetadataOnly. It does combine withPOST /{type}/$reindex, withidStart/idEnd, and with named resources.mongodb-elasticsearchnever readsmissing_params). This is checked before a job id is issued.How
ReindexTargetgainssupports_search_metadata()(defaultfalse) andwrite_search_metadata_page()(default: refuse). In metadata mode the driver writes only through writers that support it.write_metadata_pageinreindex_pipeline.rs. It reusesextract_one(on the rayon pool for large pages),update_missing_metadata(now with a counted variant) andpage_outcomes.docs/mongodb/search-indexes.md, upgrade step 3 now usesmetadataOnly, and there is a new "Metadata-only reindex" section. The definition-maintenance procedure still says full rebuild, because its value-index rows were extracted under the old definitions too. The issue suggested this mode for that case as well, but that would leave stale value rows behind.Acceptance criteria
missing_params, andsearch_indexis untouched.mongodb_metadata_only_reindex_backfills_without_touching_the_indexunsets the field across a tenant, runs the mode, and asserts that the stored arrays equal what the original writes stored. It also asserts that everysearch_index+search_index_containedrow is byte-for-byte identical, including_id, so a rewrite would show, and that a second tenant is untouched.:missing=trueresults are identical before and after. The same test checksbirthdate:missing=trueand=falseboth ways.docs/mongodb/search-indexes.md.Also covered:
skipExistingMetadatakeeps a sentinel array and respects type scope, offloaded MongoDB returnsUnsupported, plus REST parse and validation unit tests.Verified locally
cargo test -p helios-persistence --features mongodb --test mongodb_tests reindex: 81 passed, including the 3 new tests, against a real MongoDB testcontainer.cargo test -p helios-persistence --features mongodb --lib reindex: 167 passed.cargo test -p helios-rest --lib reindex: 20 passed.collapsible_ifon existing code.Not done / follow-ups
skipExistingMetadataskips only the write. Every resource in scope is still read and extracted, because the reindex source has no "field absent" filter. Pushingmissing_params: {$exists: false}into the Mongo walk (id phase and catch-up rounds) would make a mostly-backfilled tenant cheap to re-run. I left it out to keep the walk untouched.